﻿#region Copyright
// Copyright (c) 2011-2012 RaisingForce Team (Alikin Sergey)
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#endregion

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics.Contracts;
using System.Linq;
using RaisingForce.Raise.Presentation.ViewModel;

namespace RaisingForce.Raise.Presentation.ViewModel
{
    public class ItemSelectorViewModel<T> :
        ViewModelBase
        where T : class
    {
        #region Constructors

        public ItemSelectorViewModel()
            : base()
        { }

        #endregion

        #region Private Declarations

        private void CheckReadOnly()
        {
            if (this.isReadOnly)
            {
                throw new InvalidOperationException(
                    string.Format(
                        "Object {0} is in read-only state",
                        this.GetType().FullName));
                // тип объекта возвращается через GetType(),
                // поскольку классы-наследники будут иметь другое имя.
            }
        }

        #endregion

        #region Items ViewModel Property

        private ObservableCollection<T> items;

        private const string propItems = "Items";

        public ObservableCollection<T> Items
        {
            get
            {
                Contract.Ensures(Contract.Result<ObservableCollection<T>>() != null);
                return this.GetNotNullProperty(
                    ref this.items,
                    propItems,
                    this.itemsInitializer);
            }
            set
            {
                this.CheckReadOnly();
                this.SetProperty(
                    ref this.items,
                    value,
                    propItems,
                    this.itemsDeinitializer,
                    this.itemsInitializer);
            }
        }

        private static T getDefaultItem(ObservableCollection<T> items)
        {
            return items != null ? items.FirstOrDefault() : null;
        }

        private int cachedItemsCount;

        private void itemsInitializer(
            ObservableCollection<T> items)
        {
            if (items != null)
            {
                items.CollectionChanged += this.items_CollectionChanged;
            }
            T item = getDefaultItem(items);
            this.selectItem(item);
            int newItemsCount = getItemsCount(items);
            if (newItemsCount != this.cachedItemsCount)
            {
                this.cachedItemsCount = newItemsCount;
                this.OnPropertyChanged(propItemsCount);
            }
        }

        private void itemsDeinitializer(
            ObservableCollection<T> items)
        {
            if (items != null)
            {
                items.CollectionChanged -= this.items_CollectionChanged;
            }
            this.selectedItem = null;
            this.cachedItemsCount = getItemsCount(items);
        }

        private void updateItemsCount(NotifyCollectionChangedAction action)
        {
            Contract.Requires(Enum.IsDefined(typeof(NotifyCollectionChangedAction), action));
            switch (action)
            {
                case NotifyCollectionChangedAction.Add:
                case NotifyCollectionChangedAction.Remove:
                case NotifyCollectionChangedAction.Reset:
                    this.OnPropertyChanged(propItemsCount);
                    break;
            }
        }

        private void selectItem(T item)
        {
            Contract.Requires(item == null || this.ContainsItem(item));
            Contract.Ensures(this.SelectedItem == item);
            bool selectionChanged = false;
            // update item reference
            if (this.selectedItem != item)
            {
                selectionChanged = true;
                this.selectedItem = item;
                this.OnPropertyChanged(propSelectedItem);
            }
            // update item index
            int itemIndex = item != null ? this.IndexOfItem(item) : UnknownIndex;
            if (this.selectedIndex != itemIndex)
            {
                selectionChanged = true;
                this.selectedIndex = itemIndex;
                this.OnPropertyChanged(propSelectedIndex);
            }
            if (selectionChanged)
            {
                this.OnSelectionChanged();
            }
        }

        private void items_CollectionChanged(
            object sender,
            NotifyCollectionChangedEventArgs e)
        {
            T newSelectedItem;
            if (this.items.Count == 0)
            {
                newSelectedItem = null;
            }
            else if (this.selectedItem != null
                && this.items.Contains(this.selectedItem))
            {   // items.Count > 0
                newSelectedItem = this.selectedItem;
            }
            else
            {
                newSelectedItem = this.items.First();
            }
            this.selectItem(newSelectedItem);
            this.updateItemsCount(e.Action);
        }

        #endregion

        #region ViewModel Properties

        private bool isReadOnly;    // = false

        public bool IsReadOnly
        {
            get { return this.isReadOnly; }
            set
            {
                this.SetProperty(ref this.isReadOnly, value, "IsReadOnly");
            }
        }

        private T selectedItem; // = null

        private const string propSelectedItem = "SelectedItem";

        public T SelectedItem
        {
            get { return this.selectedItem; }
            set
            {
                if (this.ItemsCount > 0 && value == null)
                {
                    throw new ArgumentNullException("SelectedItem");
                }
                if (value != null && !this.ContainsItem(value))
                {
                    throw new ArgumentException(string.Format(
                        "Cannot select item \"{0}\".",
                        value.ToString()));
                }
                Contract.EndContractBlock();
                this.CheckReadOnly();
                this.selectItem(value);
            }
        }

        public const int UnknownIndex = -1;

        private int selectedIndex = UnknownIndex;

        private const string propSelectedIndex = "SelectedIndex";

        public int SelectedIndex
        {
            get
            {
                Contract.Ensures(Contract.Result<int>() == UnknownIndex || (Contract.Result<int>() >= 0 && Contract.Result<int>() < this.ItemsCount));
                return this.selectedIndex;
            }
            set
            {
                if (this.ItemsCount == 0
                    && value != UnknownIndex)
                {   // value == UnknownIndex just ignored, if ItemsCount == 0
                    throw new ArgumentException(
                        "Could not set item index for an empty collection.",
                        propSelectedIndex);
                }
                if (this.ItemsCount > 0
                    && (value < 0 || value >= this.ItemsCount))
                {
                    throw new ArgumentOutOfRangeException(propSelectedIndex);
                }
                Contract.EndContractBlock();
                this.CheckReadOnly();
                if (this.ItemsCount > 0)
                {   // condition "this.items == null" is covered in this statement
                    T item = this.items[value];
                    this.selectItem(item);
                }
            }
        }

        private const string propItemsCount = "ItemsCount";

        public static int getItemsCount(ObservableCollection<T> items)
        {
            return items != null ? items.Count : 0;
        }

        public int ItemsCount
        {
            get
            {
                Contract.Ensures(Contract.Result<int>() >= 0);
                return getItemsCount(this.items);
            }
        }

        #endregion

        #region SelectionChanged Event

        public event EventHandler SelectionChanged;

        protected virtual void OnSelectionChanged()
        {
            this.OnEvent(this.SelectionChanged);
        }

        #endregion

        #region Public Methods

        [Pure]
        public bool ContainsItem(T item)
        {
            return this.items != null ? this.items.Contains(item) : false;
        }

        [Pure]
        public int IndexOfItem(T item)
        {
            return this.items != null ? this.items.IndexOf(item) : UnknownIndex;
        }

        #endregion
    }
}
